Previous : Namespaces - Index - Next : Structs

10. Classes

A class is a data structure that may contain data members (constants and fields), function members (methods, properties, events, indexers, operators, instance constructors, destructors and static constructors), and nested types. Class types support inheritance, a mechanism whereby a derived class can extend and specialize a base class.

10.1 Class declarations

A class-declaration is a type-declaration9.5) that declares a new class.

class-declaration:
attributesopt   class-modifiersopt  
class   identifier   class-baseopt   class-body   ;opt

A class-declaration consists of an optional set of attributes17), followed by an optional set of class-modifiers10.1.1), followed by the keyword class and an identifier that names the class, followed by an optional class-base specification (§10.1.2), followed by a class-body10.1.3), optionally followed by a semicolon.

10.1.1 Class modifiers

A class-declaration may optionally include a sequence of class modifiers:

class-modifiers:
class-modifier
class-modifiers   class-modifier

class-modifier:
new
public
protected
internal

private
abstract
sealed

It is a compile-time error for the same modifier to appear multiple times in a class declaration.

The new modifier is permitted on nested classes. It specifies that the class hides an inherited member by the same name, as described in §10.2.2. It is a compile-time error for the new modifier to appear on a class declaration that is not a nested class declaration.

The public, protected, internal, and private modifiers control the accessibility of the class. Depending on the context in which the class declaration occurs, some of these modifiers may not be permitted (§3.5.1).

The abstract and sealed modifiers are discussed in the following sections.

10.1.1.1 Abstract classes

The abstract modifier is used to indicate that a class is incomplete and that it is intended to be used only as a base class. An abstract class differs from a non-abstract class in the following ways:

·         An abstract class cannot be instantiated directly, and it is a compile-time error to use the new operator on an abstract class. While it is possible to have variables and values whose compile-time types are abstract, such variables and values will necessarily either be null or contain references to instances of non-abstract classes derived from the abstract types.

·         An abstract class is permitted (but not required) to contain abstract members.

·         An abstract class cannot be sealed.

When a non-abstract class is derived from an abstract class, the non-abstract class must include actual implementations of all inherited abstract members, thereby overriding those abstract members. In the example

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

the abstract class A introduces an abstract method F. Class B introduces an additional method G, but since it doesn’t provide an implementation of F, B must also be declared abstract. Class C overrides F and provides an actual implementation. Since there are no abstract members in C, C is permitted (but not required) to be non-abstract.

10.1.1.2 Sealed classes

The sealed modifier is used to prevent derivation from a class. A compile-time error occurs if a sealed class is specified as the base class of another class.

A sealed class cannot also be an abstract class.

The sealed modifier is primarily used to prevent unintended derivation, but it also enables certain run-time optimizations. In particular, because a sealed class is known to never have any derived classes, it is possible to transform virtual function member invocations on sealed class instances into non-virtual invocations.

10.1.2 Class base specification

A class declaration may include a class-base specification, which defines the direct base class of the class and the interfaces (13) implemented by the class.

class-base:
:   class-type
:   interface-type-list
:   class-type   ,   interface-type-list

interface-type-list:
interface-type
interface-type-list  
,   interface-type

10.1.2.1 Base classes

When a class-type is included in the class-base, it specifies the direct base class of the class being declared. If a class declaration has no class-base, or if the class-base lists only interface types, the direct base class is assumed to be object. A class inherits members from its direct base class, as described in §10.2.1.

In the example

class A {}

class B: A {}

class A is said to be the direct base class of B, and B is said to be derived from A. Since A does not explicitly specify a direct base class, its direct base class is implicitly object.

The direct base class of a class type must be at least as accessible as the class type itself (§3.5.4). For example, it is a compile-time error for a public class to derive from a private or internal class.

The direct base class of a class type must not be any of the following types: System.Array, System.Delegate, System.Enum, or System.ValueType.

The base classes of a class are the direct base class and its base classes. In other words, the set of base classes is the transitive closure of the direct base class relationship. Referring to the example above, the base classes of B are A and object.

Except for class object, every class has exactly one direct base class. The object class has no direct base class and is the ultimate base class of all other classes.

When a class B derives from a class A, it is a compile-time error for A to depend on B. A class directly depends on its direct base class (if any) and directly depends on the class within which it is immediately nested (if any). Given this definition, the complete set of classes upon which a class depends is the transitive closure of the directly depends on relationship.

The example

class A: B {}

class B: C {}

class C: A {}

is in error because the classes circularly depend on themselves. Likewise, the example

class A: B.C {}

class B: A
{
   public class C {}
}

results in a compile-time error because A depends on B.C (its direct base class), which depends on B (its immediately enclosing class), which circularly depends on A.

Note that a class does not depend on the classes that are nested within it. In the example

class A
{
   class B: A {}
}

B depends on A (because A is both its direct base class and its immediately enclosing class), but A does not depend on B (since B is neither a base class nor an enclosing class of A). Thus, the example is valid.

It is not possible to derive from a sealed class. In the example

sealed class A {}

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

class B is in error because it attempts to derive from the sealed class A.

10.1.2.2 Interface implementations

A class-base specification may include a list of interface types, in which case the class is said to implement the given interface types. Interface implementations are discussed further in §13.4.

10.1.3 Class body

The class-body of a class defines the members of that class.

class-body:
{   class-member-declarationsopt   }

10.2 Class members

The members of a class consist of the members introduced by its class-member-declarations and the members inherited from the direct base class.

class-member-declarations:
class-member-declaration
class-member-declarations   class-member-declaration

class-member-declaration:
constant-declaration
field-declaration
method-declaration
property-declaration
event-declaration
indexer-declaration
operator-declaration
constructor-declaration
destructor-declaration
static-constructor-declaration
type-declaration

The members of a class are divided into the following categories:

·         Constants, which represent constant values associated with the class (§10.3).

·         Fields, which are the variables of the class (§10.4).

·         Methods, which implement the computations and actions that can be performed by the class (§10.5).

·         Properties, which define named characteristics and the actions associated with reading and writing those characteristics (§10.6).

·         Events, which define notifications that can be generated by the class (§10.7).

·         Indexers, which permit instances of the class to be indexed in the same way (syntactically) as arrays (§10.8).

·         Operators, which define the expression operators that can be applied to instances of the class (§10.9).

·         Instance constructors, which implement the actions required to initialize instances of the class (§10.10)

·         Destructors, which implement the actions to be performed before instances of the class are permanently discarded (§10.12).

·         Static constructors, which implement the actions required to initialize the class itself (§10.11).

·         Types, which represent the types that are local to the class (§9.5).

Members that can contain executable code are collectively known as the function members of the class. The function members of a class are the methods, properties, events, indexers, operators, instance constructors,  destructors, and static constructors of that class.

A class-declaration creates a new declaration space (§3.3), and the class-member-declarations immediately contained by the class-declaration introduce new members into this declaration space. The following rules apply to class-member-declarations:

·         Instance constructors, destructors and static constructors must have the same name as the immediately enclosing class. All other members must have names that differ from the name of the immediately enclosing class.

·         The name of a constant, field, property, event, or type must differ from the names of all other members declared in the same class.

·         The name of a method must differ from the names of all other non-methods declared in the same class. In addition, the signature (§3.6) of a method must differ from the signatures of all other methods declared in the same class, and two methods declared in the same class may not have signatures that differ solely by ref and out.

·         The signature of an instance constructor must differ from the signatures of all other instance constructors declared in the same class, and two constructors declared in the same class may not have signatures that differ solely by ref and out.

·         The signature of an indexer must differ from the signatures of all other indexers declared in the same class.

·         The signature of an operator must differ from the signatures of all other operators declared in the same class.

The inherited members of a class (§10.2.1) are not part of the declaration space of a class. Thus, a derived class is allowed to declare a member with the same name or signature as an inherited member (which in effect hides the inherited member).

10.2.1 Inheritance

A class inherits the members of its direct base class. Inheritance means that a class implicitly contains all members of its direct base class, except for the instance constructors, destructors and static constructors of the base class. Some important aspects of inheritance are:

·         Inheritance is transitive. If C is derived from B, and B is derived from A, then C inherits the members declared in B as well as the members declared in A.

·         A derived class extends its direct base class. A derived class can add new members to those it inherits, but it cannot remove the definition of an inherited member.

·         Instance constructors, destructors, and static constructors are not inherited, but all other members are, regardless of their declared accessibility (§3.5). However, depending on their declared accessibility, inherited members might not be accessible in a derived class.

·         A derived class can hide3.7.1.2) inherited members by declaring new members with the same name or signature. Note however that hiding an inherited member does not remove that member—it merely makes that member inaccessible directly through the derived class.

·         An instance of a class contains a set of all instance fields declared in the class and its base classes, and an implicit conversion (§6.1.4) exists from a derived class type to any of its base class types. Thus, a reference to an instance of some derived class can be treated as a reference to an instance of any of its base classes.

·         A class can declare virtual methods, properties, and indexers, and derived classes can override the implementation of these function members. This enables classes to exhibit polymorphic behavior wherein the actions performed by a function member invocation varies depending on the run-time type of the instance through which that function member is invoked.

10.2.2 The new modifier

A class-member-declaration is permitted to declare a member with the same name or signature as an inherited member. When this occurs, the derived class member is said to hide the base class member. Hiding an inherited member is not considered an error, but it does cause the compiler to issue a warning. To suppress the warning, the declaration of the derived class member can include a new modifier to indicate that the derived member is intended to hide the base member. This topic is discussed further in §3.7.1.2.

If a new modifier is included in a declaration that doesn’t hide an inherited member, a warning to that effect is issued. This warning is suppressed by removing the new modifier.

10.2.3 Access modifiers

A class-member-declaration can have any one of the five possible kinds of declared accessibility (§3.5.1): public, protected internal, protected, internal, or private. Except for the protected internal combination, it is a compile-time error to specify more than one access modifier. When a class-member-declaration does not include any access modifiers, private is assumed.

10.2.4 Constituent types

Types that are used in the declaration of a member are called the constituent types of that member. Possible constituent types are the type of a constant, field, property, event, or indexer, the return type of a method or operator, and the parameter types of a method, indexer, operator, or instance constructor. The constituent types of a member must be at least as accessible as that member itself (§3.5.4).

10.2.5 Static and instance members

Members of a class are either static members or instance members. Generally speaking, it is useful to think of static members as belonging to classes and instance members as belonging to objects (instances of classes).

When a field, method, property, event, operator, or constructor declaration includes a static modifier, it declares a static member. In addition, a constant or type declaration implicitly declares a static member. Static members have the following characteristics:

·         When a static member M is referenced in a member-access7.5.4) of the form E.M, E must denote a type containing M. It is a compile-time error for E to denote an instance.

·         A static field identifies exactly one storage location. No matter how many instances of a class are created, there is only ever one copy of a static field.

·         A static function member (method, property, event, operator, or constructor) does not operate on a specific instance, and it is a compile-time error to refer to this in such a function member.

When a field, method, property, event, indexer, constructor, or destructor declaration does not include a static modifier, it declares an instance member. (An instance member is sometimes called a non-static member.) Instance members have the following characteristics:

·         When an instance member M is referenced in a member-access7.5.4) of the form E.M, E must denote an instance of a type containing M. It is a compile-time error for E to denote a type.

·         Every instance of a class contains a separate set of all instance fields of the class.

·         An instance function member (method, property, indexer, instance constructor, or destructor) operates on a given instance of the class, and this instance can be accessed as this7.5.7).

The following example illustrates the rules for accessing static and instance members:

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

The F method shows that in an instance function member, a simple-name7.5.2) can be used to access both instance members and static members. The G method shows that in a static function member, it is a compile-time error to access an instance member through a simple-name. The Main method shows that in a member-access7.5.4), instance members must be accessed through instances, and static members must be accessed through types.

10.2.6 Nested types

A type declared within a class or struct is called a nested type. A type that is declared within a compilation unit or namespace is called a non-nested type.

In the example

using System;

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

class B is a nested type because it is declared within class A, and class A is a non-nested type because it is declared within a compilation unit.

10.2.6.1 Fully qualified name

The fully qualified name (§3.8.1) for a nested type is S.N where S is the fully qualified name of the type in which type N is declared.

10.2.6.2 Declared accessibility

Non-nested types can have public or internal declared accessibility and have internal declared accessibility by default. Nested types can have these forms of declared accessibility too, plus one or more additional forms of declared accessibility, depending on whether the containing type is a class or struct:

·         A nested type that is declared in a class can have any of five forms of declared accessibility (public, protected internal, protected, internal, or private) and, like other class members, defaults to private declared accessibility.

·         A nested type that is declared in a struct can have any of three forms of declared accessibility (public, internal, or private) and, like other struct members, defaults to private declared accessibility.

The example

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

declares a private nested class Node.

10.2.6.3 Hiding

A nested type may hide (§3.7.1) a base member. The new modifier is permitted on nested type declarations so that hiding can be expressed explicitly. The example

using System;

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

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

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

shows a nested class M that hides the method M defined in Base.

10.2.6.4 this access

A nested type and its containing type do not have a special relationship with regard to this-access7.5.7). Specifically, this within a nested type cannot be used to refer to instance members of the containing type. In cases where a nested type needs access to the instance members of its containing type, access can be provided by providing the this for the instance of the containing type as a constructor argument for the nested type. The following example

using System;

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

shows this technique. An instance of C creates an instance of Nested and passes its own this to Nested’s constructor in order to provide subsequent access to C’s instance members.

10.2.6.5 Access to private and protected members of the containing type

A nested type has access to all of the members that are accessible to its containing type, including members of the containing type that have private and protected declared accessibility. The example

using System;

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

shows a class C that contains a nested class Nested. Within Nested, the method G calls the static method F defined in C, and F has private declared accessibility.

A nested type also may access protected members defined in a base type of its containing type. In the example

using System;

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

the nested class Derived.Nested accesses the protected method F defined in Derived’s base class, Base, by calling through an instance of Derived.

10.2.7 Reserved member names

To facilitate the underlying C# runtime implementation, for each source member declaration that is a property, event, or indexer, the implementation must reserve two method signatures based on the kind of the member declaration, its name, and its type. It is a compile-time error for a program to declare a member whose signature matches one of these reserved signatures, even if the underlying runtime implementation does not make use of these reservations.

The reserved names do not introduce declarations, thus they do not participate in member lookup. However, a declaration’s associated reserved method signatures do participate in inheritance (§10.2.1), and can be hidden with the new modifier (§10.2.2).

The reservation of these names serves three purposes:

·         To allow the underlying implementation to use an ordinary identifier as a method name for get or set access to the C# language feature.

·         To allow other languages to interoperate using an ordinary identifier as a method name for get or set access to the C# language feature.

·         To help ensure that the source accepted by one conforming compiler is accepted by another, by making the specifics of reserved member names consistent across all C# implementations.

The declaration of a destructor (§10.12) also causes a signature to be reserved (§10.2.7.4).

10.2.7.1 Member names reserved for properties

For a property P10.6) of type T, the following signatures are reserved:

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

Both signatures are reserved, even if the property is read-only or write-only.

In the example

using System;

class A
{
   public int P {
      get { return 123; }
   }
}

class B: A
{
   new public int get_P() {
      return 456;
   }

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

a class A defines a read-only property P, thus reserving signatures for get_P and set_P methods. A class B derives from A and hides both of these reserved signatures. The example produces the output:

123
123
456

10.2.7.2 Member names reserved for events

For an event E10.7) of delegate type T, the following signatures are reserved:

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

10.2.7.3 Member names reserved for indexers

For an indexer (§10.8) of type T with parameter-list L, the following signatures are reserved:

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

Both signatures are reserved, even if the indexer is read-only or write-only.

10.2.7.4 Member names reserved for destructors

For a class containing a destructor (§10.12), the following signature is reserved:

void Finalize();

10.3 Constants

A constant is a class member that represents a constant value: a value that can be computed at compile-time. A constant-declaration introduces one or more constants of a given type.

constant-declaration:
attributesopt   constant-modifiersopt   const   type   constant-declarators   ;

constant-modifiers:
constant-modifier
constant-modifiers   constant-modifier

constant-modifier:
new
public
protected
internal

private

constant-declarators:
constant-declarator
constant-declarators  
,   constant-declarator

constant-declarator:
identifier   =   constant-expression

A constant-declaration may include a set of attributes17), a new modifier (§10.2.2), and a valid combination of the four access modifiers (§10.2.3). The attributes and modifiers apply to all of the members declared by the constant-declaration. Even though constants are considered static members, a constant-declaration neither requires nor allows a static modifier. It is an error for the same modifier to appear multiple times in a constant declaration.

The type of a constant-declaration specifies the type of the members introduced by the declaration. The type is followed by a list of constant-declarators, each of which introduces a new member. A constant-declarator consists of an identifier that names the member, followed by an “=” token, followed by a constant-expression7.15) that gives the value of the member.

The type specified in a constant declaration must be sbyte, byte, short, ushort, int, uint, long, ulong, char, float, double, decimal, bool, string, an enum-type, or a reference-type. Each constant-expression must yield a value of the target type or of a type that can be converted to the target type by an implicit conversion (§6.1).

The type of a constant must be at least as accessible as the constant itself (§3.5.4).

The value of a constant is obtained in an expression using a simple-name7.5.2) or a member-access7.5.4).

A constant can itself participate in a constant-expression. Thus, a constant may be used in any construct that requires a constant-expression. Examples of such constructs include case labels, goto case statements, enum member declarations, attributes, and other constant declarations.

As described in §7.15, a constant-expression is an expression that can be fully evaluated at compile-time. Since the only way to create a non-null value of a reference-type other than string is to apply the new operator, and since the new operator is not permitted in a constant-expression, the only possible value for constants of reference-types other than string is null.

When a symbolic name for a constant value is desired, but when the type of that value is not permitted in a constant declaration, or when the value cannot be computed at compile-time by a constant-expression, a readonly field (§10.4.2) may be used instead.

A constant declaration that declares multiple constants is equivalent to multiple declarations of single constants with the same attributes, modifiers, and type. For example

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

is equivalent to

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

Constants are permitted to depend on other constants within the same program as long as the dependencies are not of a circular nature. The compiler automatically arranges to evaluate the constant declarations in the appropriate order. In the example

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

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

the compiler first evaluates A.Y, then evaluates B.Z, and finally evaluates A.X, producing the values 10, 11, and 12. Constant declarations may depend on constants from other programs, but such dependencies are only possible in one direction. Referring to the example above, if A and B were declared in separate programs, it would be possible for A.X to depend on B.Z, but B.Z could then not simultaneously depend on A.Y.

10.4 Fields

A field is a member that represents a variable associated with an object or class. A field-declaration introduces one or more fields of a given type.

field-declaration:
attributesopt   field-modifiersopt   type   variable-declarators  
;

field-modifiers:
field-modifier
field-modifiers   field-modifier

field-modifier:
new
public
protected
internal

private
static
readonly
volatile

variable-declarators:
variable-declarator
variable-declarators  
,   variable-declarator

variable-declarator:
identifier
identifier   =   variable-initializer

variable-initializer:
expression
array-initializer

A field-declaration may include a set of attributes17), a new modifier (§10.2.2), a valid combination of the four access modifiers (§10.2.3), and a static modifier (§10.4.1). In addition, a field-declaration may include a readonly modifier (§10.4.2) or a volatile modifier (§10.4.3) but not both. The attributes and modifiers apply to all of the members declared by the field-declaration. It is an error for the same modifier to appear multiple times in a field declaration.

The type of a field-declaration specifies the type of the members introduced by the declaration. The type is followed by a list of variable-declarators, each of which introduces a new member. A variable-declarator consists of an identifier that names that member, optionally followed by an “=” token and a variable-initializer10.4.5) that gives the initial value of that member.

The type of a field must be at least as accessible as the field itself (§3.5.4).

The value of a field is obtained in an expression using a simple-name7.5.2) or a member-access7.5.4). The value of a non-readonly field is modified using an assignment7.13). The value of a non-readonly field can be both obtained and modified using postfix increment and decrement operators (§7.5.9) and prefix increment and decrement operators (§7.6.5).

A field declaration that declares multiple fields is equivalent to multiple declarations of single fields with the same attributes, modifiers, and type. For example

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

is equivalent to

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

10.4.1 Static and instance fields

When a field declaration includes a static modifier, the fields introduced by the declaration are static fields. When no static modifier is present, the fields introduced by the declaration are instance fields. Static fields and instance fields are two of the several kinds of variables (§5) supported by C#, and at times they are referred to as static variables and instance variables, respectively.

A static field is not part of a specific instance; instead, it identifies exactly one storage location. No matter how many instances of a class are created, there is only ever one copy of a static field for the associated application domain.

An instance field belongs to an instance. Specifically, every instance of a class contains a separate set of all the instance fields of that class.

When a field is referenced in a member-access7.5.4) of the form E.M, if M is a static field, E must denote a type containing M, and if M is an instance field, E must denote an instance of a type containing M.

The differences between static and instance members are discussed further in §10.2.5.

10.4.2 Readonly fields

When a field-declaration includes a readonly modifier, the fields introduced by the declaration are readonly fields. Direct assignments to readonly fields can only occur as part of that declaration or in an instance constructor or static constructor in the same class. (A readonly field can be assigned to multiple times in these contexts.) Specifically, direct assignments to a readonly field are permitted only in the following contexts:

·         In the variable-declarator that introduces the field (by including a variable-initializer in the declaration).

·         For an instance field, in the instance constructors of the class that contains the field declaration; for a static field, in the static constructor of the class that contains the field declaration. These are also the only contexts in which it is valid to pass a readonly field as an out or ref parameter.

Attempting to assign to a readonly field or pass it as an out or ref parameter in any other context is a compile-time error.

10.4.2.1 Using static readonly fields for constants

A static readonly field is useful when a symbolic name for a constant value is desired, but when the type of the value is not permitted in a const declaration, or when the value cannot be computed at compile-time. In the example

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

the Black, White, Red, Green, and Blue members cannot be declared as const members because their values cannot be computed at compile-time. However, declaring them static readonly instead has much the same effect.

10.4.2.2 Versioning of constants and static readonly fields

Constants and readonly fields have different binary versioning semantics. When an expression references a constant, the value of the constant is obtained at compile-time, but when an expression references a readonly field, the value of the field is not obtained until run-time. Consider an application that consists of two separate programs:

using System;

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

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

The Program1 and Program2 namespaces denote two programs that are compiled separately. Because Program1.Utils.X is declared as a static readonly field, the value output by the Console.WriteLine statement is not known at compile-time, but rather is obtained at run-time. Thus, if the value of X is changed and Program1 is recompiled, the Console.WriteLine statement will output the new value even if Program2 isn’t recompiled. However, had X been a constant, the value of X would have been obtained at the time Program2 was compiled, and would remain unaffected by changes in Program1 until Program2 is recompiled.

10.4.3 Volatile fields

When a field-declaration includes a volatile modifier, the fields introduced by that declaration are volatile fields.

For non-volatile fields, optimization techniques that reorder instructions can lead to unexpected and unpredictable results in multi-threaded programs that access fields without synchronization such as that provided by the lock-statement8.12). These optimizations can be performed by the compiler, by the runtime system, or by hardware. For volatile fields, such reordering optimizations are restricted:

·         A read of a volatile field is called a volatile read. A volatile read has “acquire semantics”; that is, it is guaranteed to occur prior to any references to memory that occur after it in the instruction sequence.

·         A write of a volatile field is called a volatile write. A volatile write has “release semantics”; that is, it is guaranteed to happen after any memory references prior to the write instruction in the instruction sequence.

These restrictions ensure that all threads will observe volatile writes performed by any other thread in the order in which they were performed. A conforming implementation is not required to provide a single total ordering of volatile writes as seen from all threads of execution. The type of a volatile field must be one of the following:

·         A reference-type.

·         The type byte, sbyte, short, ushort, int, uint, char, float, or bool.

·         An enum-type having an enum base type of byte, sbyte, short, ushort, int, or uint.

The example

using System;
using System.Threading;

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 = {0}", result);
            return;
         }
      }
   }
}

produces the output:

result = 143

In this example, the method Main starts a new thread that runs the method Thread2. This method stores a value into a non-volatile field called result, then stores true in the volatile field finished. The main thread waits for the field finished to be set to true, then reads the field result. Since finished has been declared volatile, the main thread must read the value 143 from the field result. If the field finished had not been declared volatile, then it would be permissible for the store to result to be visible to the main thread after the store to finished, and hence for the main thread to read the value 0 from the field result. Declaring finished as a volatile field prevents any such inconsistency.

10.4.4 Field initialization

The initial value of a field, whether it be a static field or an instance field, is the default value (§5.2) of the field’s type. It is not possible to observe the value of a field before this default initialization has occurred, and a field is thus never “uninitialized”. The example

using System;

class Test
{
   static bool b;
   int i;

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

produces the output

b = False, i = 0

because b and i are both automatically initialized to default values.

10.4.5 Variable initializers

Field declarations may include variable-initializers. For static fields, variable initializers correspond to assignment statements that are executed during class initialization. For instance fields, variable initializers correspond to assignment statements that are executed when an instance of the class is created.

The example

using System;

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 = {0}, i = {1}, s = {2}", x, a.i, a.s);
   }
}

produces the output

x = 1.4142135623731, i = 100, s = Hello

because an assignment to x occurs when static field initializers execute and assignments to i and s occur when the instance field initializers execute.

The default value initialization described in §10.4.4 occurs for all fields, including fields that have variable initializers. Thus, when a class is initialized, all static fields in that class are first initialized to their default values, and then the static field initializers are executed in textual order. Likewise, when an instance of a class is created, all instance fields in that instance are first initialized to their default values, and then the instance field initializers are executed in textual order.

It is possible for static fields with variable initializers to be observed in their default value state. However, this is strongly discouraged as a matter of style. The example

using System;

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

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

exhibits this behavior. Despite the circular definitions of a and b, the program is valid. It results in the output

a = 1, b = 2

because the static fields a and b are initialized to 0 (the default value for int) before their initializers are executed. When the initializer for a runs, the value of b is zero, and so a is initialized to 1. When the initializer for b runs, the value of a is already 1, and so b is initialized to 2.

10.4.5.1 Static field initialization

The static field variable initializers of a class correspond to a sequence of assignments that are executed in the textual order in which they appear in the class declaration. If a static constructor (§10.11) exists in the class, execution of the static field initializers occurs immediately prior to executing that static constructor. Otherwise, the static field initializers are executed at an implementation-dependent time prior to the first use of a static field of that class. The example

using System;

class Test
{
   static void Main() {
      Console.WriteLine("{0} {1}", 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");
}

might produce either the output:

Init A
Init B
1 1

or the output:

Init B
Init A
1 1

because the execution of X’s initializer and Y’s initializer could occur in either order; they are only constrained to occur before the references to those fields. However, in the example:

using System;

class Test
{
   static void Main() {
      Console.WriteLine("{0} {1}", 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");
}

the output must be:

Init B
Init A
1 1

because the rules for when static constructors execute (as defined in §10.11) provide that B’s static constructor (and hence B’s static field initializers) must run before A’s static constructor and field initializers.

10.4.5.2 Instance field initialization

The instance field variable initializers of a class correspond to a sequence of assignments that are executed immediately upon entry to any one of the instance constructors (§10.10.1) of that class. The variable initializers are executed in the textual order in which they appear in the class declaration. The class instance creation and initialization process is described further in §10.10.

A variable initializer for an instance field cannot reference the instance being created. Thus, it is a compile-time error to reference this in a variable initializer, as it is a compile-time error for a variable initializer to reference any instance member through a simple-name. In the example

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

the variable initializer for y results in a compile-time error because it references a member of the instance being created.

10.5 Methods

A method is a member that implements a computation or action that can be performed by an object or class. Methods are declared using method-declarations:

method-declaration:
method-header   method-body

method-header:
attributesopt   method-modifiersopt   return-type   member-name  
(   formal-parameter-listopt   )

method-modifiers:
method-modifier
method-modifiers   method-modifier

method-modifier:
new
public
protected
internal
private
static
virtual
sealed
override
abstract
extern

return-type:
type
void

member-name:
identifier
interface-type  
.   identifier

method-body:
block
;

A method-declaration may include a set of attributes17) and a valid combination of the four access modifiers (§10.2.3), the new10.2.2),  static10.5.2), virtual10.5.3), override10.5.4), sealed10.5.5), abstract10.5.6), and extern10.5.7) modifiers.

A declaration has a valid combination of modifiers if all of the following are true:

·         The declaration includes a valid combination of access modifiers (§10.2.3).

·         The declaration does not include the same modifier multiple times.

·         The declaration includes at most one of the following modifiers: static, virtual, and override.

·         The declaration includes at most one of the following modifiers: new and override.

·         If the declaration includes the abstract modifier, then the declaration does not include any of the following modifiers: static, virtual, sealed or extern.

·         If the declaration includes the private modifier, then the declaration does not include any of the following modifiers: virtual, override, or abstract.

·         If the declaration includes the sealed modifier, then the declaration also includes the override modifier.

The return-type of a method declaration specifies the type of the value computed and returned by the method. The return-type is void if the method does not return a value.

The member-name specifies the name of the method. Unless the method is an explicit interface member implementation (§13.4.1), the member-name is simply an identifier. For an explicit interface member implementation, the member-name consists of an interface-type followed by a “.” and an identifier.

The optional formal-parameter-list specifies the parameters of the method (§10.5.1).

The return-type and each of the types referenced in the formal-parameter-list of a method must be at least as accessible as the method itself (§3.5.4).

For abstract and extern methods, the method-body consists simply of a semicolon. For all other methods, the method-body consists of a block, which specifies the statements to execute when the method is invoked.

The name and the formal parameter list of a method define the signature (§3.6) of the method. Specifically, the signature of a method consists of its name and the number, modifiers, and types of its formal parameters. The return type is not part of a method’s signature, nor are the names of the formal parameters.

The name of a method must differ from the names of all other non-methods declared in the same class. In addition, the signature of a method must differ from the signatures of all other methods declared in the same class, and two methods declared in the same class may not have signatures that differ solely by ref and out.

10.5.1 Method parameters

The parameters of a method, if any, are declared by the method’s formal-parameter-list.

formal-parameter-list:
fixed-parameters
fixed-parameters  
,   parameter-array
parameter-array

fixed-parameters:
fixed-parameter
fixed-parameters  
,   fixed-parameter

fixed-parameter:
attributesopt   parameter-modifieropt   type   identifier

parameter-modifier:
ref
out

parameter-array:
attributesopt  
params   array-type   identifier

The formal parameter list consists of one or more comma-separated parameters of which only the last may be a parameter-array.

A fixed-parameter consists of an optional set of attributes17), an optional ref or out modifier, a type, and an identifier. Each fixed-parameter declares a parameter of the given type with the given name.

A parameter-array consists of an optional set of attributes17), a params modifier, an array-type, and an identifier. A parameter array declares a single parameter of the given array type with the given name. The array-type of a parameter array must be a single-dimensional array type (§12.1). In a method invocation, a parameter array permits either a single argument of the given array type to be specified, or it permits zero or more arguments of the array element type to be specified. Parameter arrays are described further in §10.5.1.4.

A method declaration creates a separate declaration space for parameters and local variables. Names are introduced into this declaration space by the formal parameter list of the method and by local variable declarations in the block of the method. All names in the declaration space of a method must be unique; otherwise, a compile-time error results.

A method invocation (§7.5.5.1) creates a copy, specific to that invocation, of the formal parameters and local variables of the method, and the argument list of the invocation assigns values or variable references to the newly created formal parameters. Within the block of a method, formal parameters can be referenced by their identifiers in simple-name expressions (§7.5.2).

There are four kinds of formal parameters:

·         Value parameters, which are declared without any modifiers.

·         Reference parameters, which are declared with the ref modifier.

·         Output parameters, which are declared with the out modifier.

·         Parameter arrays, which are declared with the params modifier.

As described in §3.6, the ref and out modifiers are part of a method’s signature, but the params modifier is not.

10.5.1.1 Value parameters

A parameter declared with no modifiers is a value parameter. A value parameter corresponds to a local variable that gets its initial value from the corresponding argument supplied in the method invocation.

When a formal parameter is a value parameter, the corresponding argument in a method invocation must be an expression of a type that is implicitly convertible (§6.1) to the formal parameter type.

A method is permitted to assign new values to a value parameter. Such assignments only affect the local storage location represented by the value parameter—they have no effect on the actual argument given in the method invocation.

10.5.1.2 Reference parameters

A parameter declared with a ref modifier is a reference parameter. Unlike a value parameter, a reference parameter does not create a new storage location. Instead, a reference parameter represents the same storage location as the variable given as the argument in the method invocation.

When a formal parameter is a reference parameter, the corresponding argument in a method invocation must consist of the keyword ref followed by a variable-reference5.3.3) of the same type as the formal parameter. A variable must be definitely assigned before it can be passed as a reference parameter.

Within a method, a reference parameter is always considered definitely assigned.

The example

using System;

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 = {0}, j = {1}", i, j);
   }
}

produces the output

i = 2, j = 1

For the invocation of Swap in Main, x represents i and y represents j. Thus, the invocation has the effect of swapping the values of i and j.

In a method that takes reference parameters it is possible for multiple names to represent the same storage location. In the example

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

the invocation of F in G passes a reference to s for both a and b. Thus, for that invocation, the names s, a, and b all refer to the same storage location, and the three assignments all modify the instance field s.

10.5.1.3 Output parameters

A parameter declared with an out modifier is an output parameter. Similar to a reference parameter, an output parameter does not create a new storage location. Instead, an output parameter represents the same storage location as the variable given as the argument in the method invocation.

When a formal parameter is an output parameter, the corresponding argument in a method invocation must consist of the keyword out followed by a variable-reference5.3.3) of the same type as the formal parameter. A variable need not be definitely assigned before it can be passed as an output parameter, but following an invocation where a variable was passed as an output parameter, the variable is considered definitely assigned.

Within a method, just like a local variable, an output parameter is initially considered unassigned and must be definitely assigned before its value is used.

Every output parameter of a method must be definitely assigned before the method returns.

Output parameters are typically used in methods that produce multiple return values. For example:

using System;

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

The example produces the output:

c:\Windows\System\
hello.txt

Note that the dir and name variables can be unassigned before they are passed to SplitPath, and that they are considered definitely assigned following the call.

10.5.1.4 Parameter arrays

A parameter declared with a params modifier is a parameter array. If a formal parameter list includes a parameter array, it must be the last parameter in the list and it must be of a single-dimensional array type. For example, the types string[] and string[][] can be used as the type of a parameter array, but the type string[,] can not. It is not possible to combine the params modifier with the modifiers ref and out.

A parameter array permits arguments to be specified in one of two ways in a method invocation:

·         The argument given for a parameter array can be a single expression of a type that is implicitly convertible (§6.1) to the parameter array type. In this case, the parameter array acts precisely like a value parameter.

·         Alternatively, the invocation can specify zero or more arguments for the parameter array, where each argument is an expression of a type that is implicitly convertible (§6.1) to the element type of the parameter array. In this case, the invocation creates an instance of the parameter array type with a length corresponding to the number of arguments, initializes the elements of the array instance with the given argument values, and uses the newly created array instance as the actual argument.

Except for allowing a variable number of arguments in an invocation, a parameter array is precisely equivalent to a value parameter (§10.5.1.1) of the same type.

The example

using System;

class Test
{
   static void F(params int[] args) {
      Console.Write("Array contains {0} elements:", args.Length);
      foreach (int i in args)
         Console.Write(" {0}", i);
      Console.WriteLine();
   }

   static void Main() {
      int[] arr = {1, 2, 3};
      F(arr);
      F(10, 20, 30, 40);
      F();
   }
}

produces the output

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

The first invocation of F simply passes the array a as a value parameter. The second invocation of F automatically creates a four-element int[] with the given element values and passes that array instance as a value parameter. Likewise, the third invocation of F creates a zero-element int[] and passes that instance as a value parameter. The second and third invocations are precisely equivalent to writing:

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

When performing overload resolution, a method with a parameter array may be applicable either in its normal form or in its expanded form (§7.4.2.1). The expanded form of a method is available only if the normal form of the method is not applicable and only if a method with the same signature as the expanded form is not already declared in the same type.

The example

using System;

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

produces the output

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

In the example, two of the possible expanded forms of the method with a parameter array are already included in the class as regular methods. These expanded forms are therefore not considered when performing overload resolution, and the first and third method invocations thus select the regular methods. When a class declares a method with a parameter array, it is not uncommon to also include some of the expanded forms as regular methods. By doing so it is possible to avoid the allocation of an array instance that occurs when an expanded form of a method with a parameter array is invoked.

When the type of a parameter array is object[], a potential ambiguity arises between the normal form of the method and the expended form for a single object parameter. The reason for the ambiguity is that an object[] is itself implicitly convertible to type object. The ambiguity presents no problem, however, since it can be resolved by inserting a cast if needed.

The example

using System;

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

produces the output

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

In the first and last invocations of F, the normal form of F is applicable because an implicit conversion exists from the argument type to the parameter type (both are of type object[]). Thus, overload resolution selects the normal form of F, and the argument is passed as a regular value parameter. In the second and third invocations, the normal form of F is not applicable because no implicit conversion exists from the argument type to the parameter type (type object cannot be implicitly converted to type object[]). However, the expanded form of F is applicable, so it is selected by overload resolution. As a result, a one-element object[] is created by the invocation, and the single element of the array is initialized with the given argument value (which itself is a reference to an object[]).

10.5.2 Static and instance methods

When a method declaration includes a static modifier, that method is said to be a static method. When no static modifier is present, the method is said to be an instance method.

A static method does not operate on a specific instance, and it is a compile-time error to refer to this in a static method.

An instance method operates on a given instance of a class, and that instance can be accessed as this7.5.7).

When a method is referenced in a member-access7.5.4) of the form E.M, if M is a static method, E must denote a type containing M, and if M is an instance method, E must denote an instance of a type containing M.

The differences between static and instance members are discussed further in §10.2.5.

10.5.3 Virtual methods

When an instance method declaration includes a virtual modifier, that method is said to be a virtual method. When no virtual modifier is present, the method is said to be a non-virtual method.

The implementation of a non-virtual method is invariant: The implementation is the same whether the method is invoked on an instance of the class in which it is declared or an instance of a derived class. In contrast, the implementation of a virtual method can be superseded by derived classes. The process of superseding the implementation of an inherited virtual method is known as overriding that method (§10.5.4).

In a virtual method invocation, the run-time type of the instance for which that invocation takes place determines the actual method implementation to invoke. In a non-virtual method invocation, the compile-time type of the instance is the determining factor. In precise terms, when a method named N is invoked with an argument list A on an instance with a compile-time type C and a run-time type R (where R is either C or a class derived from C), the invocation is processed as follows:

·         First, overload resolution is applied to C, N, and A, to select a specific method M from the set of methods declared in and inherited by C. This is described in §7.5.5.1.

·         Then, if M is a non-virtual method, M is invoked.

·         Otherwise, M is a virtual method, and the most derived implementation of M with respect to R is invoked.

For every virtual method declared in or inherited by a class, there exists a most derived implementation of the method with respect to that class. The most derived implementation of a virtual method M with respect to a class R is determined as follows:

·         If R contains the introducing virtual declaration of M, then this is the most derived implementation of M.

·         Otherwise, if R contains an override of M, then this is the most derived implementation of M.

·         Otherwise, the most derived implementation of M with respect to R is the same as the most derived implementation of M with respect to the direct base class of R.

The following example illustrates the differences between virtual and non-virtual methods:

using System;

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

   public virtual void G() { Console.WriteLine("A.G"); }
}

class B: A
{
   new public 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();
   }
}

In the example, A introduces a non-virtual method F and a virtual method G. The class B introduces a new non-virtual method F, thus hiding the inherited F, and also overrides the inherited method G. The example produces the output:

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

Notice that the statement a.G() invokes B.G, not A.G. This is because the run-time type of the instance (which is B), not the compile-time type of the instance (which is A), determines the actual method implementation to invoke.

Because methods are allowed to hide inherited methods, it is possible for a class to contain several virtual methods with the same signature. This does not present an ambiguity problem, since all but the most derived method are hidden. In the example

using System;

class A
{
   public virtual void F() { Console.WriteLine("A.F"); }
}

class B: A
{
   public override void F() { Console.WriteLine("B.F"); }
}

class C: B
{
   new public 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();
   }
}

the C and D classes contain two virtual methods with the same signature: The one introduced by A and the one introduced by C. The method introduced by C hides the method inherited from A. Thus, the override declaration in D overrides the method introduced by C, and it is not possible for D to override the method introduced by A. The example produces the output:

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

Note that it is possible to invoke the hidden virtual method by accessing an instance of D through a less derived type in which the method is not hidden.

10.5.4 Override methods

When an instance method declaration includes an override modifier, the method is said to be an override method. An override method overrides an inherited virtual method with the same signature. Whereas a virtual method declaration introduces a new method, an override method declaration specializes an existing inherited virtual method by providing a new implementation of that method.

The method overridden by an override declaration is known as the overridden base method. For an override method M declared in a class C, the overridden base method is determined by examining each base class of C, starting with the direct base class of C and continuing with each successive direct base class, until an accessible method with the same signature as M is located. For the purposes of locating the overridden base method, a method is considered accessible if it is public, if it is protected, if it is protected internal, or if it is internal and declared in the same program as C.

A compile-time error occurs unless all of the following are true for an override declaration:

·         An overridden base method can be located as described above.

·         The overridden base method is a virtual, abstract, or override method. In other words, the overridden base method cannot be static or non-virtual.

·         The overridden base method is not a sealed method.

·         The override declaration and the overridden base method have the same return type.

·         The override declaration and the overridden base method have the same declared accessibility. In other words, an override declaration cannot change the accessibility of the virtual method.

An override declaration can access the overridden base method using a base-access7.5.8). In the example

class A
{
   int x;

   public virtual void PrintFields() {
      Console.WriteLine("x = {0}", x);
   }
}

class B: A
{
   int y;

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

the base.PrintFields() invocation in B invokes the PrintFields method declared in A. A base-access disables the virtual invocation mechanism and simply treats the base method as a non-virtual method. Had the invocation in B been written ((A)this).PrintFields(), it would recursively invoke the PrintFields method declared in B, not the one declared in A, since PrintFields is virtual and the run-time type of ((A)this) is B.

Only by including an override modifier can a method override another method. In all other cases, a method with the same signature as an inherited method simply hides the inherited method. In the example

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

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

the F method in B does not include an override modifier and therefore does not override the F method in A. Rather, the F method in B hides the method in A, and a warning is reported because the declaration does not include a new modifier.

In the example

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

class B: A
{
   new private void F() {}       // Hides A.F within B
}

class C: B
{
   public override void F() {}   // Ok, overrides A.F
}

the F method in B hides the virtual F method inherited from A. Since the new F in B has private access, its scope only includes the class body of B and does not extend to C. Therefore, the declaration of F in C is permitted to override the F inherited from A.

10.5.5 Sealed methods

When an instance method declaration includes a sealed modifier, that method is said to be a sealed method. If an instance method declaration includes the  sealed modifier, it must also include the override modifier. Use of the sealed modifier prevents a derived class from further overriding the method.

The example

using System;

class A
{
   public virtual void F() {
      Console.WriteLine("A.F");
   }

   public virtual void G() {
      Console.WriteLine("A.G");
   }
}

class B: A
{
   sealed override public void F() {
      Console.WriteLine("B.F");
   }

   override public void G() {
      Console.WriteLine("B.G");
   }
}

class C: B
{
   override public void G() {
      Console.WriteLine("C.G");
   }
}

the class B provides two override methods: an F method that has the sealed modifier and a G method that does not. B’s use of the sealed modifier prevents C from further overriding F.

10.5.6 Abstract methods

When an instance method declaration includes an abstract modifier, that method is said to be an abstract method. Although an abstract method is implicitly also a virtual method, it cannot have the modifier virtual.

An abstract method declaration introduces a new virtual method but does not provide an implementation of that method. Instead, non-abstract derived classes are required to provide their own implementation by overriding that method. Because an abstract method provides no actual implementation, the method-body of an abstract method simply consists of a semicolon.

Abstract method declarations are only permitted in abstract classes (§10.1.1.1).

In the example

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

the Shape class defines the abstract notion of a geometrical shape object that can paint itself. The Paint method is abstract because there is no meaningful default implementation. The Ellipse and Box classes are concrete Shape implementations. Because these classes are non-abstract, they are required to override the Paint method and provide an actual implementation.

It is a compile-time error for a base-access7.5.8) to reference an abstract method. In the example

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

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

a compile-time error is reported for the base.F() invocation because it references an abstract method.

An abstract method declaration is permitted to override a virtual method. This allows an abstract class to force re-implementation of the method in derived classes, and makes the original implementation of the method unavailable. In the example

using System;

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 declares a virtual method, class B overrides this method with an abstract method, and class C overrides the abstract method to provide its own implementation.

10.5.7 External methods

When a method declaration includes an extern modifier, that method is said to be an external method. External methods are implemented externally, typically using a language other than C#. Because an external method declaration provides no actual implementation, the method-body of an external method simply consists of a semicolon.

The extern modifier is typically used in conjunction with a DllImport attribute (§17.5.1), allowing external methods to be implemented by DLLs (Dynamic Link Libraries). The execution environment may support other mechanisms whereby implementations of external methods can be provided.

When an external method includes a DllImport attribute, the method declaration must also include a static modifier. This example demonstrates the use of the extern modifier and the DllImport attribute:

using System.Text;
using System.Security.Permissions;
using System.Runtime.InteropServices;

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

10.5.8 Method body

The method-body of a method declaration consists of either a block or a semicolon.

Abstract and external method declarations do not provide a method implementation, so their method bodies simply consist of a semicolon. For any other method, the method body is a block (§8.2) that contains the statements to execute when that method is invoked.

When the return type of a method is void, return statements (§8.9.4) in that method’s body are not permitted to specify an expression. If execution of the method body of a void method completes normally (that is, control flows off the end of the method body), that method simply returns to its caller.

When the return type of a method is not void, each return statement in that method’s body must specify an expression of a type that is implicitly convertible to the return type. The endpoint of the method body of a value-returning method must not be reachable. In other words, in a value-returning method, control is not permitted to flow off the end of the method body.

In the example

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

the value-returning F method results in a compile-time error because control can flow off the end of the method body. The G and H methods are correct because all possible execution paths end in a return statement that specifies a return value.

10.5.9 Method overloading

The method overload resolution rules are described in §7.4.2.

10.6 Properties

A property is a member that provides access to a characteristic of an object or a class. Examples of properties include the length of a string, the size of a font, the caption of a window, the name of a customer, and so on. Properties are a natural extension of fields—both are named members with associated types, and the syntax for accessing fields and properties is the same. However, unlike fields, properties do not denote storage locations. Instead, properties have accessors that specify the statements to be executed when their values are read or written. Properties thus provide a mechanism for associating actions with the reading and writing of an object’s attributes; furthermore, they permit such attributes to be computed.

Properties are declared using property-declarations:

property-declaration:
attributesopt   property-modifiersopt   type   member-name  
{   accessor-declarations   }

property-modifiers:
property-modifier
property-modifiers   property-modifier

property-modifier:
new
public
protected
internal
private
static
virtual
sealed
override
abstract
extern

member-name:
identifier
interface-type  
.   identifier

A property-declaration may include a set of attributes17) and a valid combination of the four access modifiers (§10.2.3), the new10.2.2),  static10.5.2), virtual10.5.3), override10.5.4), sealed10.5.5), abstract10.5.6), and extern10.5.7) modifiers.

Property declarations are subject to the same rules as method declarations (§10.5) with regard to valid combinations of modifiers.

The type of a property declaration specifies the type of the property introduced by the declaration, and the member-name specifies the name of the property. Unless the property is an explicit interface member implementation, the member-name is simply an identifier. For an explicit interface member implementation (§13.4.1), the member-name consists of an interface-type followed by a “.” and an identifier.

The type of a property must be at least as accessible as the property itself (§3.5.4).

The accessor-declarations, which must be enclosed in “{” and “}” tokens, declare the accessors (§10.6.2) of the property. The accessors specify the executable statements associated with reading and writing the property.

Even though the syntax for accessing a property is the same as that for a field, a property is not classified as a variable. Thus, it is not possible to pass a property as a ref or out argument.

When a property declaration includes an extern modifier, the property is said to be an external property. Because an external property declaration provides no actual implementation, each of its accessor-declarations consists of a semicolon.

10.6.1 Static and instance properties

When a property declaration includes a static modifier, the property is said to be a static property. When no static modifier is present, the property is said to be an instance property.

A static property is not associated with a specific instance, and it is a compile-time error to refer to this in the accessors of a static property.

An instance property is associated with a given instance of a class, and that instance can be accessed as this7.5.7) in the accessors of that property.

When a property is referenced in a member-access7.5.4) of the form E.M, if M is a static property, E must denote a type containing M, and if M is an instance property, E must denote an instance of a type containing M.

The differences between static and instance members are discussed further in §10.2.5.

10.6.2 Accessors

The accessor-declarations of a property specify the executable statements associated with reading and writing that property.

accessor-declarations:
get-accessor-declaration   set-accessor-declarationopt
set-accessor-declaration   get-accessor-declarationopt

get-accessor-declaration:
attributesopt  
get   accessor-body

set-accessor-declaration:
attributesopt  
set   accessor-body

accessor-body:
block
;

The accessor declarations consist of a get-accessor-declaration, a set-accessor-declaration, or both. Each accessor declaration consists of the token get or set followed by an accessor-body. For abstract and extern properties, the accessor-body for each accessor specified is simply a semicolon. For the accessors of any non-abstract, non-extern property, the accessor-body is a block which specifies the statements to be executed when the corresponding accessor is invoked.

A get accessor corresponds to a parameterless method with a return value of the property type. Except as the target of an assignment, when a property is referenced in an expression, the get accessor of the property is invoked to compute the value of the property (§7.1.1). The body of a get accessor must conform to the rules for value-returning methods described in §10.5.8. In particular, all return statements in the body of a get accessor must specify an expression that is implicitly convertible to the property type. Furthermore, the endpoint of a get accessor must not be reachable.

A set accessor corresponds to a method with a single value parameter of the property type and a void return type. The implicit parameter of a set accessor is always named value. When a property is referenced as the target of an assignment (§7.13), or as the operand of ++ or --7.5.9, §7.6.5), the set accessor is invoked with an argument (whose value is that of the right-hand side of the assignment or the operand of the ++ or -- operator) that provides the new value (§7.13.1). The body of a set accessor must conform to the rules for void methods described in §10.5.8. In particular, return statements in the set accessor body are not permitted to specify an expression. Since a set accessor implicitly has a parameter named value, it is a compile-time error for a local variable or constant declaration in a set accessor to have that name.

Based on the presence or absence of the get and set accessors, a property is classified as follows:

·         A property that includes both a get accessor and a set accessor is said to be a read-write property.

·         A property that has only a get accessor is said to be a read-only property. It is a compile-time error for a read-only property to be the target of an assignment.

·         A property that has only a set accessor is said to be a write-only property. Except as the target of an assignment, it is a compile-time error to reference a write-only property in an expression.

In the example

public class Button: Control
{
   private string caption;

   public string Caption {
      get {
         return caption;
      }
      set {
         if (caption != value) {
            caption = value;
            Repaint();
         }
      }
   }

   public override void Paint(Graphics g, Rectangle r) {
      // Painting code goes here
   }
}

the Button control declares a public Caption property. The get accessor of the Caption property returns the string stored in the private caption field. The set accessor checks if the new value is different from the current value, and if so, it stores the new value and repaints the control. Properties often follow the pattern shown above: The get accessor simply returns a value stored in a private field, and the set accessor modifies that private field and then performs any additional actions required to fully update the state of the object.

Given the Button class above, the following is an example of use of the Caption property:

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

Here, the set accessor is invoked by assigning a value to the property, and the get accessor is invoked by referencing the property in an expression.

The get and set accessors of a property are not distinct members, and it is not possible to declare the accessors of a property separately. As such, it is not possible for the two accessors of a read-write property to have different accessibility. The example

class A
{
   private string name;

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

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

does not declare a single read-write property. Rather, it declares two properties with the same name, one read-only and one write-only. Since two members declared in the same class cannot have the same name, the example causes a compile-time error to occur.

When a derived class declares a property by the same name as an inherited property, the derived property hides the inherited property with respect to both reading and writing. In the example

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

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

the P property in B hides the P property in A with respect to both reading and writing. Thus, in the statements

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

the assignment to b.P causes a compile-time error to be reported, since the read-only P property in B hides the write-only P property in A. Note, however, that a cast can be used to access the hidden P property.

Unlike public fields, properties provide a separation between an object’s internal state and its public interface. Consider the example:

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 {
      get { return x; }
   }

   public int Y {
      get { return y; }
   }

   public Point Location {
      get { return new Point(x, y); }
   }

   public string Caption {
      get { return caption; }
   }
}

Here, the Label class uses two int fields, x and y, to store its location. The location is publicly exposed both as an X and a Y property and as a Location property of type Point. If, in a future version of Label, it becomes more convenient to store the location as a Point internally, the change can be made without affecting the public interface of the class:

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 {
      get { return location.x; }
   }

   public int Y {
      get { return location.y; }
   }

   public Point Location {
      get { return location; }
   }

   public string Caption {
      get { return caption; }
   }
}

Had x and y instead been public readonly fields, it would have been impossible to make such a change to the Label class.

Exposing state through properties is not necessarily any less efficient than exposing fields directly. In particular, when a property is non-virtual and contains only a small amount of code, the execution environment may replace calls to accessors with the actual code of the accessors. This process is known as inlining, and it makes property access as efficient as field access, yet preserves the increased flexibility of properties.

Since invoking a get accessor is conceptually equivalent to reading the value of a field, it is considered bad programming style for get accessors to have observable side-effects. In the example

class Counter
{
   private int next;

   public int Next {
      get { return next++; }
   }
}

the value of the Next property depends on the number of times the property has previously been accessed. Thus, accessing the property produces an observable side-effect, and the property should be implemented as a method instead.

The “no side-effects” convention for get accessors doesn’t mean that get accessors should always be written to simply return values stored in fields. Indeed, get accessors often compute the value of a property by accessing multiple fields or invoking methods. However, a properly designed get accessor performs no actions that cause observable changes in the state of the object.

Properties can be used to delay initialization of a resource until the moment it is first referenced. For example:

using System.IO;

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

The Console class contains three properties, In, Out, and Error, that represent the standard input, output, and error devices, respectively. By exposing these members as properties, the Console class can delay their initialization until they are actually used. For example, upon first referencing the Out property, as in

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

the underlying TextWriter for the output device is created. But if the application makes no reference to the In and Error properties, then no objects are created for those devices.

10.6.3 Virtual, sealed, override, and abstract accessors

A virtual property declaration specifies that the accessors of the property are virtual. The virtual modifier applies to both accessors of a read-write property—it is not possible for only one accessor of a read-write property to be virtual.

An abstract property declaration specifies that the accessors of the property are virtual, but does not provide an actual implementation of the accessors. Instead, non-abstract derived classes are required to provide their own implementation for the accessors by overriding the property. Because an accessor for an abstract property declaration provides no actual implementation, its accessor-body simply consists of a semicolon.

A property declaration that includes both the abstract and override modifiers specifies that the property is abstract and overrides a base property. The accessors of such a property are also abstract.

Abstract property declarations are only permitted in abstract classes (§10.1.1.1).The accessors of an inherited virtual property can be overridden in a derived class by including a property declaration that specifies an override directive. This is known as an overriding property declaration. An overriding property declaration does not declare a new property. Instead, it simply specializes the implementations of the accessors of an existing virtual property.

An overriding property declaration must specify the exact same accessibility modifiers, type, and name as the inherited property. If the inherited property has only a single accessor (i.e., if the inherited property is read-only or write-only), the overriding property must include only that accessor. If the inherited property includes both accessors (i.e., if the inherited property is read-write), the overriding property can include either a single accessor or both accessors.

An overriding property declaration may include the sealed modifier. Use of this modifier prevents a derived class from further overriding the property. The accessors of a sealed property are also sealed.

Except for differences in declaration and invocation syntax, virtual, sealed, override, and abstract accessors behave exactly like virtual, sealed, override and abstract methods. Specifically, the rules described in §10.5.3, §10.5.4, §10.5.5, and §10.5.6 apply as if accessors were methods of a corresponding form:

·         A get accessor corresponds to a parameterless method with a return value of the property type and the same modifiers as the containing property.

·         A set accessor corresponds to a method with a single value parameter of the property type, a void return type, and the same modifiers as the containing property.

In the example

abstract class A
{
   int y;

   public virtual int X {
      get { return 0; }
   }

   public virtual int Y {
      get { return y; }
      set { y = value; }
   }

   public abstract int Z { get; set; }
}

X is a virtual read-only property, Y is a virtual read-write property, and Z is an abstract read-write property. Because Z is abstract, the containing class A must also be declared abstract.

A class that derives from A is show below:

class B: A
{
   int z;

   public override int X {
      get { return base.X + 1; }
   }

   public override int Y {
      set { base.Y = value < 0? 0: value; }
   }

   public override int Z {
      get { return z; }
      set { z = value; }
   }
}

Here, the declarations of X, Y, and Z are overriding property declarations. Each property declaration exactly matches the accessibility modifiers, type, and name of the corresponding inherited property. The get accessor of X and the set accessor of Y use the base keyword to access the inherited accessors. The declaration of Z overrides both abstract accessors—thus, there are no outstanding abstract function members in B, and B is permitted to be a non-abstract class.

10.7 Events

An event is a member that enables an object or class to provide notifications. Clients can attach executable code for events by supplying event handlers.

Events are declared using event-declarations:

event-declaration:
attributesopt   event-modifiersopt  
event   type   variable-declarators   ;
attributesopt   event-modifiersopt   event   type   member-name   {   event-accessor-declarations   }

event-modifiers:
event-modifier
event-modifiers   event-modifier

event-modifier:
new
public
protected
internal
private
static
virtual
sealed
override
abstract
extern

event-accessor-declarations:
add-accessor-declaration   remove-accessor-declaration
remove-accessor-declaration   add-accessor-declaration

add-accessor-declaration:
attributesopt  
add   block

remove-accessor-declaration:
attributesopt  
remove   block

An event-declaration may include a set of attributes17) and a valid combination of the four access modifiers (§10.2.3), the new10.2.2),  static10.5.2), virtual10.5.3), override10.5.4), sealed10.5.5), abstract10.5.6), and extern10.5.7) modifiers.

Event declarations are subject to the same rules as method declarations (§10.5) with regard to valid combinations of modifiers.

The type of an event declaration must be a delegate-type4.2), and that delegate-type must be at least as accessible as the event itself (§3.5.4).

An event declaration may include event-accessor-declarations. However, if it does not, for non-extern, non-abstract events, the compiler supplies them automatically (§10.7.1); for extern events, the accessors are provided externally.

An event declaration that omits event-accessor-declarations defines one or more events—one for each of the variable-declarators. The attributes and modifiers apply to all of the members declared by such an event-declaration.

It is a compile-time error for an event-declaration to include both the abstract modifier and brace-delimited event-accessor-declarations.

When an event declaration includes an extern modifier, the event is said to be an external event. Because an external event declaration provides no actual implementation, it is an error for it to include both the extern modifier and event-accessor-declarations.

An event can be used as the left-hand operand of the += and -= operators (§7.13.3). These operators are used, respectively, to attach event handlers to or to remove event handlers from an event, and the access modifiers of the event control the contexts in which such operations are permitted.

Since += and -= are the only operations that are permitted on an event outside the type that declares the event, external code can add and remove handlers for an event, but cannot in any other way obtain or modify the underlying list of event handlers.

In an operation of the form x += y or x -= y, when x is an event and the reference takes place outside the type that contains the declaration of x, the result of the operation has type void (as opposed to having the type of x, with the value of x after the assignment). This rule prohibits external code from indirectly examining the underlying delegate of an event.

The following example shows how event handlers are attached to instances of the Button class:

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

Here, the LoginDialog instance constructor creates two Button instances and attaches event handlers to the Click events.

10.7.1 Field-like events

Within the program text of the class or struct that contains the declaration of an event, certain events can be used like fields. To be used in this way, an event must not be abstract or extern, and must not explicitly include event-accessor-declarations. Such an event can be used in any context that permits a field. The field contains a delegate (§15) which refers to the list of event handlers that have been added to the event. If no event handlers have been added, the field contains null.

In the example

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

public class Button: Control
{
   public event EventHandler Click;

   protected void OnClick(EventArgs e) {
      if (Click != null) Click(this, e);
   }

   public void Reset() {
      Click = null;
   }
}

Click is used as a field within the Button class. As the example demonstrates, the field can be examined, modified, and used in delegate invocation expressions. The OnClick method in the Button class “raises” the Click event. The notion of raising an event is precisely equivalent to invoking the delegate represented by the event—thus, there are no special language constructs for raising events. Note that the delegate invocation is preceded by a check that ensures the delegate is non-null.

Outside the declaration of the Button class, the Click member can only be used on the left-hand side of the += and –= operators, as in

b.Click += new EventHandler();

which appends a delegate to the invocation list of the Click event, and

b.Click –= new EventHandler();

which removes a delegate from the invocation list of the Click event.

When compiling a field-like event, the compiler automatically creates storage to hold the delegate, and creates accessors for the event that add or remove event handlers to the delegate field. In order to be thread-safe, the addition or removal operations are done while holding the lock (§8.12) on the containing object for an instance event, or the type object (§7.5.11) for a static event.

Thus, an instance event declaration of the form:

class X
{
   public event D Ev;
}

could be compiled to something equivalent to:

class X
{
   private D __Ev;  // field to hold the delegate

   public event D Ev {
      add {
         lock(this) { __Ev = __Ev + value; }
      }

      remove {
         lock(this) { __Ev = __Ev - value; }
      }
   }
}

Within the class X, references to Ev are compiled to reference the hidden field __Ev instead. The name “__Ev” is arbitrary; the hidden field could have any name or no name at all.

Similarly, a static event declaration of the form:

class X
{
   public static event D Ev;
}

could be compiled to something equivalent to:

class X
{
   private static D __Ev;  // field to hold the delegate

   public static event D Ev {
      add {
         lock(typeof(X)) { __Ev = __Ev + value; }
      }

      remove {
         lock(typeof(X)) { __Ev = __Ev - value; }
      }
   }
}

10.7.2 Event accessors

Event declarations typically omit event-accessor-declarations, as in the Button example above. One situation for doing so involves the case in which the storage cost of one field per event is not acceptable. In such cases, a class can include event-accessor-declarations and use a private mechanism for storing the list of event handlers.

The event-accessor-declarations of an event specify the executable statements associated with adding and removing event handlers.

The accessor declarations consist of an add-accessor-declaration and a remove-accessor-declaration. Each accessor declaration consists of the token add or remove followed by a block. The block associated with an add-accessor-declaration specifies the statements to execute when an event handler is added, and the block associated with a remove-accessor-declaration specifies the statements to execute when an event handler is removed.

Each add-accessor-declaration and remove-accessor-declaration corresponds to a method with a single value parameter of the event type and a void return type. The implicit parameter of an event accessor is named value. When an event is used in an event assignment, the appropriate event accessor is used. Specifically, if the assignment operator is += then the add accessor is used, and if the assignment operator is -= then the remove accessor is used. In either case, the right-hand operand of the assignment operator is used as the argument to the event accessor. The block of an add-accessor-declaration or a remove-accessor-declaration must conform to the rules for void methods described in §10.5.8. In particular, return statements in such a block are not permitted to specify an expression.

Since an event accessor implicitly has a parameter named value, it is a compile-time error for a local variable or constant declared in an event accessor to have that name.

In the example

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

the Control class implements an internal storage mechanism for events. The AddEventHandler method associates a delegate value with a key, the GetEventHandler method returns the delegate currently associated with a key, and the RemoveEventHandler method removes a delegate as an event handler for the specified event. Presumably, the underlying storage mechanism is designed such that there is no cost for associating a null delegate value with a key, and thus unhandled events consume no storage.

10.7.3 Static and instance events

When an event declaration includes a static modifier, the event is said to be a static event. When no static modifier is present, the event is said to be an instance event.

A static event is not associated with a specific instance, and it is a compile-time error to refer to this in the accessors of a static event.

An instance event is associated with a given instance of a class, and this instance can be accessed as this7.5.7) in the accessors of that event.

When an event is referenced in a member-access7.5.4) of the form E.M, if M is a static event, E must denote a type containing M, and if M is an instance event, E must denote an instance of a type containing M.

The differences between static and instance members are discussed further in §10.2.5.

10.7.4 Virtual, sealed, override, and abstract accessors

A virtual event declaration specifies that the accessors of that event are virtual. The virtual modifier applies to both accessors of an event.

An abstract event declaration specifies that the accessors of the event are virtual, but does not provide an actual implementation of the accessors. Instead, non-abstract derived classes are required to provide their own implementation for the accessors by overriding the event. Because an accessor for an abstract event declaration provides no actual implementation, its accessor-body simply consists of a semicolon.

An event declaration that includes both the abstract and override modifiers specifies that the event is abstract and overrides a base event. The accessors of such an event are also abstract.

Abstract event declarations are only permitted in abstract classes (§10.1.1.1).

The accessors of an inherited virtual event can be overridden in a derived class by including an event declaration that specifies an override modifier. This is known as an overriding event declaration. An overriding event declaration does not declare a new event. Instead, it simply specializes the implementations of the accessors of an existing virtual event.

An overriding event declaration must specify the exact same accessibility modifiers, type, and name as the overridden event.

An overriding event declaration may include the sealed modifier. Use of this modifier prevents a derived class from further overriding the event. The accessors of a sealed event are also sealed.

It is a compile-time error for an overriding event declaration to include a new modifier.

Except for differences in declaration and invocation syntax, virtual, sealed, override, and abstract accessors behave exactly like virtual, sealed, override and abstract methods. Specifically, the rules described in §10.5.3, §10.5.4, §10.5.5, and §10.5.6 apply as if accessors were methods of a corresponding form. Each accessor corresponds to a method with a single value parameter of the event type, a void return type, and the same modifiers as the containing event.

10.8 Indexers

An indexer is a member that enables an object to be indexed in the same way as an array. Indexers are declared using indexer-declarations:

indexer-declaration:
attributesopt   indexer-modifiersopt   indexer-declarator  
{   accessor-declarations   }

indexer-modifiers:
indexer-modifier
indexer-modifiers   indexer-modifier

indexer-modifier:
new
public
protected
internal
private
virtual
sealed
override
abstract
extern

indexer-declarator:
type  
this   [   formal-parameter-list   ]
type   interface-type  
.   this   [   formal-parameter-list   ]

An indexer-declaration may include a set of attributes17) and a valid combination of the four access modifiers (§10.2.3), the new10.2.2), virtual10.5.3), override10.5.4), sealed10.5.5), abstract10.5.6), and extern10.5.7) modifiers.

Indexer declarations are subject to the same rules as method declarations (§10.5) with regard to valid combinations of modifiers, with the one exception being that the static modifier is not permitted on an indexer declaration.

The modifiers virtual, override, and abstract are mutually exclusive except in one case. The abstract and override modifiers may be used together so that an abstract indexer can override a virtual one.

The type of an indexer declaration specifies the element type of the indexer introduced by the declaration. Unless the indexer is an explicit interface member implementation, the type is followed by the keyword this. For an explicit interface member implementation, the type is followed by an interface-type, a “.”, and the keyword this. Unlike other members, indexers do not have user-defined names.

The formal-parameter-list specifies the parameters of the indexer. The formal parameter list of an indexer corresponds to that of a method (§10.5.1), except that at least one parameter must be specified, and that the ref and out parameter modifiers are not permitted.

The type of an indexer and each of the types referenced in the formal-parameter-list must be at least as accessible as the indexer itself (§3.5.4).

The accessor-declarations10.6.2), which must be enclosed in “{” and “}” tokens, declare the accessors of the indexer. The accessors specify the executable statements associated with reading and writing indexer elements.

Even though the syntax for accessing an indexer element is the same as that for an array element, an indexer element is not classified as a variable. Thus, it is not possible to pass an indexer element as a ref or out argument.

The formal parameter list of an indexer defines the signature (§3.6) of the indexer. Specifically, the signature of an indexer consists of the number and types of its formal parameters. The element type and names of the formal parameters are not part of an indexer’s signature.

The signature of an indexer must differ from the signatures of all other indexers declared in the same class.

Indexers and properties are very similar in concept, but differ in the following ways:

·         A property is identified by its name, whereas an indexer is identified by its signature.

·         A property is accessed through a simple-name7.5.2) or a member-access7.5.4), whereas an indexer element is accessed through an element-access7.5.6.2).

·         A property can be a static member, whereas an indexer is always an instance member.

·         A get accessor of a property corresponds to a method with no parameters, whereas a get accessor of an indexer corresponds to a method with the same formal parameter list as the indexer.

·         A set accessor of a property corresponds to a method with a single parameter named value, whereas a set accessor of an indexer corresponds to a method with the same formal parameter list as the indexer, plus an additional parameter named value.

·         It is a compile-time error for an indexer accessor to declare a local variable with the same name as an indexer parameter.

·         In an overriding property declaration, the inherited property is accessed using the syntax base.P, where P is the property name. In an overriding indexer declaration, the inherited indexer is accessed using the syntax base[E], where E is a comma separated list of expressions.

Aside from these differences, all rules defined in §10.6.2 and §10.6.3 apply to indexer accessors as well as to property accessors.

When an indexer declaration includes an extern modifier, the indexer is said to be an external indexer. Because an external indexer declaration provides no actual implementation, each of its accessor-declarations consists of a semicolon.

The example below declares a BitArray class that implements an indexer for accessing the individual bits in the bit array.

using System;

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 {
      get { return 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);
         }
      }
   }
}

An instance of the BitArray class consumes substantially less memory than a corresponding bool[] (since each value of the former occupies only one bit instead of the latter’s one byte), but it permits the same operations as a bool[].

The following CountPrimes class uses a BitArray and the classical “sieve” algorithm to compute the number of primes between 1 and a given maximum:

class CountPrimes
{
   static int Count(int max) {
      BitArray flags = new BitArray(max + 1);
      int count = 1;
      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 {0} primes between 1 and {1}", count, max);
   }
}

Note that the syntax for accessing elements of the BitArray is precisely the same as for a bool[].

The following example shows a 26 ´ 10 grid class that has an indexer with two parameters. The first parameter is required to be an upper- or lowercase letter in the range A–Z, and the second is required to be an integer in the range 0–9.

using System;

class Grid
{
   const int NumRows = 26;
   const int NumCols = 10;

   int[,] cells = new int[NumRows, NumCols];

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

      set {
         c = Char.ToUpper(c);
         if (c < 'A' || c > 'Z') {
            throw new ArgumentException();
         }
         if (col < 0 || col >= NumCols) {
            throw new IndexOutOfRangeException();
         }
         cells[c - 'A', col] = value;
      }
   }
}

10.8.1 Indexer overloading

The indexer overload resolution rules are described in §7.4.2.

10.9 Operators

An operator is a member that defines the meaning of an expression operator that can be applied to instances of the class. Operators are declared using operator-declarations:

operator-declaration:
attributesopt   operator-modifiers   operator-declarator   operator-body

operator-modifiers:
operator-modifier
operator-modifiers   operator-modifier

operator-modifier:
public
static
extern

operator-declarator:
unary-operator-declarator
binary-operator-declarator
conversion-operator-declarator

unary-operator-declarator:
type  
operator   overloadable-unary-operator   (   type   identifier   )

overloadable-unary-operator:  one of
+   -   !   ~   ++   --   true   false

binary-operator-declarator:
type  
operator   overloadable-binary-operator   (   type   identifier   ,   type   identifier   )

overloadable-binary-operator:  one of
+   -   *   /   %   &   |   ^   <<   >>   ==   !=   >   <   >=   <=

conversion-operator-declarator:
implicit
   operator   type   (   type   identifier   )
explicit   operator   type   (   type   identifier   )

operator-body:
block
;

There are three categories of overloadable operators: Unary operators (§10.9.1), binary operators (§10.9.2), and conversion operators (§10.9.3).

When an operator declaration includes an extern modifier, the operator is said to be an external operator. Because an external operator provides no actual implementation, its operator-body consists of a semi-colon. For all other operators, the operator-body consists of a block, which specifies the statements to execute when the operator is invoked. The block of an operator must conform to the rules for value-returning methods described in §10.5.8.

The following rules apply to all operator declarations:

·         An operator declaration must include both a public and a static modifier.

·         The parameter(s) of an operator must be value parameters. It is a compile-time error for an operator declaration to specify ref or out parameters.

·         The signature of an operator (§10.9.1, §10.9.2, §10.9.3) must differ from the signatures of all other operators declared in the same class.

·         All types referenced in an operator declaration must be at least as accessible as the operator itself (§3.5.4).

·         It is an error for the same modifier to appear multiple times in an operator declaration.

Each operator category imposes additional restrictions, as described in the following sections.

Like other members, operators declared in a base class are inherited by derived classes. Because operator declarations always require the class or struct in which the operator is declared to participate in the signature of the operator, it is not possible for an operator declared in a derived class to hide an operator declared in a base class. Thus, the new modifier is never required, and therefore never permitted, in an operator declaration.

Additional information on unary and binary operators can be found in §7.2.

Additional information on conversion operators can be found in §6.4.

10.9.1 Unary operators

The following rules apply to unary operator declarations, where T denotes the class or struct type that contains the operator declaration:

·         A unary +, -, !, or ~ operator must take a single parameter of type T and can return any type.

·         A unary ++ or -- operator must take a single parameter of type T and must return type T.

·         A unary true or false operator must take a single parameter of type T and must return type bool.

The signature of a unary operator consists of the operator token (+, -, !, ~, ++, --, true, or false) and the type of the single formal parameter. The return type is not part of a unary operator’s signature, nor is the name of the formal parameter.

The true and false unary operators require pair-wise declaration. A compile-time error occurs if a class declares one of these operators without also declaring the other. The true and false operators are described further in §7.11.2 and §7.16.

The following example shows an implementation and subsequent usage of operator ++ for an integer vector class:

public class IntVector
{
   public IntVector(int length) {...}

   public int Length {...}             // read-only property

   public int this[int index] {...}    // 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
   }
}

Note how the operator method returns the value produced by adding 1 to the operand, just like the  postfix increment and decrement operators (§7.5.9), and the prefix increment and decrement operators (§7.6.5). Unlike in C++, this method need not modify the value of its operand directly. In fact, modifying the operand value would violate the standard semantics of the postfix increment operator.

10.9.2 Binary operators

A binary operator must take two parameters, at least one of which must have the class or struct type in which the operator is declared. The shift operators (§7.8) are further constrained: The type of the first parameter must be the class or struct type in which the operator is declared, and the second parameter must always have the type int. A binary operator can return any type.

The signature of a binary operator consists of the operator token (+, -, *, /, %, &, |, ^, <<, >>, ==, !=, >, <, >=, or <=) and the types of the two formal parameters. The return type and the names of the formal parameters are not part of a binary operator’s signature.

Certain binary operators require pair-wise declaration. For every declaration of either operator of a pair, there must be a matching declaration of the other operator of the pair. Two operator declarations match when they have the same return type and the same type for each parameter. The following operators require pair-wise declaration:

·         operator == and operator !=

·         operator > and operator <

·         operator >= and operator <=

10.9.3 Conversion operators

A conversion operator declaration introduces a user-defined conversion6.4) which augments the pre-defined implicit and explicit conversions.

A conversion operator declaration that includes the implicit keyword introduces a user-defined implicit conversion. Implicit conversions can occur in a variety of situations, including function member invocations, cast expressions, and assignments. This is described further in §6.1.

A conversion operator declaration that includes the explicit keyword introduces a user-defined explicit conversion. Explicit conversions can occur in cast expressions, and are described further in §6.2.

A conversion operator converts from a source type, indicated by the parameter type of the conversion operator, to a target type, indicated by the return type of the conversion operator. A class or struct is permitted to declare a conversion from a source type S to a target type T provided all of the following are true:

·         S and T are different types.

·         Either S or T is the class or struct type in which the operator declaration takes place.

·         Neither S nor T is object or an interface-type.

·         T is not a base class of S, and S is not a base class of T.

From the second rule it follows that a conversion operator must convert either to or from the class or struct type in which the operator is declared. For example, it is possible for a class or struct type C to define a conversion from C to int and from int to C, but not from int to bool.

It is not possible to redefine a pre-defined conversion. Thus, conversion operators are not allowed to convert from or to object because implicit and explicit conversions already exist between object and all other types. Likewise, neither the source nor the target types of a conversion can be a base type of the other, since a conversion would then already exist.

User-defined conversions are not allowed to convert from or to interface-types. In particular, this restriction ensures that no user-defined transformations occur when converting to an interface-type, and that a conversion to an interface-type succeeds only if the object being converted actually implements the specified interface-type.

The signature of a conversion operator consists of the source type and the target type. (Note that this is the only form of member for which the return type participates in the signature.) The implicit or explicit classification of a conversion operator is not part of the operator’s signature. Thus, a class or struct cannot declare both an implicit and an explicit conversion operator with the same source and target types.

In general, user-defined implicit conversions should be designed to never throw exceptions and never lose information. If a user-defined conversion can give rise to exceptions (for example, because the source argument is out of range) or loss of information (such as discarding high-order bits), then that conversion should be defined as an explicit conversion.

In the example

using System;

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) {
      return d.value;
   }

   public static explicit operator Digit(byte b) {
      return new Digit(b);
   }
}

the conversion from Digit to byte is implicit because it never throws exceptions or loses information, but the conversion from byte to Digit is explicit since Digit can only represent a subset of the possible values of a byte.

10.10 Instance constructors

An instance constructor is a member that implements the actions required to initialize an instance of a class. Instance constructors are declared using constructor-declarations:

constructor-declaration:
attributesopt   constructor-modifiersopt   constructor-declarator   constructor-body

constructor-modifiers:
constructor-modifier
constructor-modifiers   constructor-modifier

constructor-modifier:
public
protected
internal

private
extern

constructor-declarator:
identifier  
(   formal-parameter-listopt   )   constructor-initializeropt

constructor-initializer:
:   base   (   argument-listopt   )
:
   this   (   argument-listopt   )

constructor-body:
block
;

A constructor-declaration may include a set of attributes17), a valid combination of the four access modifiers (§10.2.3), and an extern10.5.7) modifier. A constructor declaration is not permitted to include the same modifier multiple times.

The identifier of a constructor-declarator must name the class in which the instance constructor is declared. If any other name is specified, a compile-time error occurs.

The optional formal-parameter-list of an instance constructor is subject to the same rules as the formal-parameter-list of a method (§10.5). The formal parameter list defines the signature (§3.6) of an instance constructor and governs the process whereby overload resolution (§7.4.2) selects a particular instance constructor in an invocation.

Each of the types referenced in the formal-parameter-list of an instance constructor must be at least as accessible as the constructor itself (§3.5.4).

The optional constructor-initializer specifies another instance constructor to invoke before executing the statements given in the constructor-body of this instance constructor. This is described further in §10.10.1.

When a constructor declaration includes an extern modifier, the constructor is said to be an external constructor. Because an external constructor declaration provides no actual implementation, its constructor-body consists of a semicolon. For all other constructors, the constructor-body consists of a block which specifies the statements to initialize a new instance of the class. This corresponds exactly to the block of an instance method with a void return type (§10.5.8).

Instance constructors are not inherited. Thus, a class has no instance constructors other than those actually declared in the class. If a class contains no instance constructor declarations, a default instance constructor is automatically provided (§10.10.4).

Instance constructors are invoked by object-creation-expressions (§7.5.10.1) and through constructor-initializers.

10.10.1 Constructor initializers

All instance constructors (except those for class object) implicitly include an invocation of another instance constructor immediately before the constructor-body. The constructor to implicitly invoke is determined by the constructor-initializer:

·         An instance constructor initializer of the form base(argument-listopt) causes an instance constructor from the direct base class to be invoked. That constructor is selected using argument-list and the overload resolution rules of §7.4.2. The set of candidate instance constructors consists of all accessible instance constructors contained in the direct base class, or the default constructor (§10.10.4), if no instance constructors are declared in the direct base class. If this set is empty, or if a single best instance constructor cannot be identified, a compile-time error occurs.

·         An instance constructor initializer of the form this(argument-listopt) causes an instance constructor from the class itself to be invoked. The constructor is selected using argument-list and the overload resolution rules of §7.4.2. The set of candidate instance constructors consists of all accessible instance constructors declared in the class itself. If this set is empty, or if a single best instance constructor cannot be identified, a compile-time error occurs. If an instance constructor declaration includes a constructor initializer that invokes the constructor itself, a compile-time error occurs.

If an instance constructor has no constructor initializer, a constructor initializer of the form base() is implicitly provided. Thus, an instance constructor declaration of the form

C(...) {...}

is exactly equivalent to

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

The scope of the parameters given by the formal-parameter-list of an instance constructor declaration includes the constructor initializer of that declaration. Thus, a constructor initializer is permitted to access the parameters of the constructor. For example:

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

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

An instance constructor initializer cannot access the instance being created. Therefore it is a compile-time error to reference this in an argument expression of the constructor initializer, as is it a compile-time error for an argument expression to reference any instance member through a simple-name.

10.10.2 Instance variable initializers

When an instance constructor has no constructor initializer, or it has a constructor initializer of the form base(...), that constructor implicitly performs the initializations specified by the variable-initializers of the instance fields declared in its class. This corresponds to a sequence of assignments that are executed immediately upon entry to the constructor and before the implicit invocation of the direct base class constructor. The variable initializers are executed in the textual order in which they appear in the class declaration.

10.10.3 Constructor execution

Variable initializers are transformed into assignment statements, and these assignment statements are executed before the invocation of the base class instance constructor. This ordering ensures that all instance fields are initialized by their variable initializers before any statements that have access to that instance are executed.

Given the example

using System;

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 = {0}, y = {1}", x, y);
   }
}

when new B() is used to create an instance of B, the following output is produced:

x = 1, y = 0

The value of x is 1 because the variable initializer is executed before the base class instance constructor is invoked. However, the value of y is 0 (the default value of an int) because the assignment to y is not executed until after the base class constructor returns.

It is useful to think of instance variable initializers and constructor initializers as statements that are automatically inserted before the constructor-body. The example

using System;
using System.Collections;

class A
{
   int x = 1, y = -1, count;

   public A() {
      count = 0;
   }

   public A(int n) {
      count = n;
   }
}

class B: A
{
   double sqrt2 = Math.Sqrt(2.0);
   ArrayList items = new ArrayList(100);
   int max;

   public B(): this(100) {
      items.Add("default");
   }

   public B(int n): base(n – 1) {
      max = n;
   }
}

contains several variable initializers; it also contains constructor initializers of both forms (base and this). The example corresponds to the code shown below, where each comment indicates an automatically inserted statement (the syntax used for the automatically inserted constructor invocations isn’t valid, but merely serves to illustrate the mechanism).

using System.Collections;

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

10.10.4 Default constructors

If a class contains no instance constructor declarations, a default instance constructor is automatically provided. That default constructor simply invokes the parameterless constructor of the direct base class. If the direct base class does not have an accessible parameterless instance constructor, a compile-time error occurs. If the class is abstract then the declared accessibility for the default constructor is protected. Otherwise, the declared accessibility for the default constructor is public. Thus, the default constructor is always of the form

protected C(): base() {}

or

public C(): base() {}

where C is the name of the class.

In the example

class Message
{
   object sender;
   string text;
}

a default constructor is provided because the class contains no instance constructor declarations. Thus, the example is precisely equivalent to

class Message
{
   object sender;
   string text;

   public Message(): base() {}
}

10.10.5 Private constructors

When a class T declares only private instance constructors, it is not possible for classes outside the program text of T to derive from T or to directly create instances of T. Thus, if a class contains only static members and isn’t intended to be instantiated, adding an empty private instance constructor will prevent instantiation. For example:

public class Trig
{
   private Trig() {}    // Prevent instantiation

   public const double PI = 3.14159265358979323846;

   public static double Sin(double x) {...}
   public static double Cos(double x) {...}
   public static double Tan(double x) {...}
}

The Trig class groups related methods and constants, but is not intended to be instantiated. Therefore it declares a single empty private instance constructor. At least one instance constructor must be declared to suppress the automatic generation of a default constructor.

10.10.6 Optional instance constructor parameters

The this(...) form of constructor initializer is commonly used in conjunction with overloading to implement optional instance constructor parameters. In the example

class Text
{
   public Text(): this(0, 0, null) {}

   public Text(int x, int y): this(x, y, null) {}

   public Text(int x, int y, string s) {
      // Actual constructor implementation
   }
}

the first two instance constructors merely provide the default values for the missing arguments. Both use a this(...) constructor initializer to invoke the third instance constructor, which actually does the work of initializing the new instance. The effect is that of optional constructor parameters:

Text t1 = new Text();               // Same as Text(0, 0, null)
Text t2 = new Text(5, 10);          // Same as Text(5, 10, null)
Text t3 = new Text(5, 20, "Hello");

10.11 Static constructors

A static constructor is a member that implements the actions required to initialize a class. Static constructors are declared using static-constructor-declarations:

static-constructor-declaration:
attributesopt   static-constructor-modifiers  identifier  
(   )   static-constructor-body

static-constructor-modifiers:
externopt   static
static   externopt

static-constructor-body:
block
;

A static-constructor-declaration may include a set of attributes17) and an extern modifier (§10.5.7).

The identifier of a static-constructor-declaration must name the class in which the static constructor is declared. If any other name is specified, a compile-time error occurs.

When a static constructor declaration includes an extern modifier, the static constructor is said to be an external static constructor. Because an external static constructor declaration provides no actual implementation, its static-constructor-body consists of a semicolon. For all other static constructor declarations, the static-constructor-body consists of a block which specifies the statements to execute in order to initialize the class. This corresponds exactly to the method-body of a static method with a void return type (§10.5.8).

Static constructors are not inherited, and cannot be called directly.

The static constructor for a class executes at most once in a given application domain. The execution of a static constructor is triggered by the first of the following events to occur within an application domain:

·         An instance of the class is created.

·         Any of the static members of the class are referenced.

If a class contains the Main method (§3.1) in which execution begins, the static constructor for that class executes before the Main method is called. If a class contains any static fields with initializers, those initializers are executed in textual order immediately prior to executing the static constructor.

The example

using System;

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

must produce the output:

Init A
A.F
Init B
B.F

because the execution of A’s static constructor is triggered by the call to A.F, and the execution of B’s static constructor is triggered by the call to B.F.

It is possible to construct circular dependencies that allow static fields with variable initializers to be observed in their default value state.

The example

using System;

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 = {0}, Y = {1}", A.X, B.Y);
   }
}

produces the output

X = 1, Y = 2

To execute the Main method, the system first runs the initializer for B.Y, prior to class B’s static constructor. Y’s initializer causes A’s static constructor to be run because the value of A.X is referenced. The static constructor of A in turn proceeds to compute the value of X, and in doing so fetches the default value of Y, which is zero. A.X is thus initialized to 1. The process of running A’s static field initializers and static constructor then completes, returning to the calculation of the initial value of Y, the result of which becomes 2.

10.12 Destructors

A destructor is a member that implements the actions required to destruct an instance of a class. A destructor is declared using a destructor-declaration:

destructor-declaration:
attributesopt  
externopt   ~   identifier   (   )    destructor-body

destructor-body:
block
;

A destructor-declaration may include a set of attributes17).

The identifier of a destructor-declarator must name the class in which the destructor is declared. If any other name is specified, a compile-time error occurs.

When a destructor declaration includes an extern modifier, the destructor is said to be an external destructor. Because an external destructor declaration provides no actual implementation, its destructor-body consists of a semicolon. For all other destructors, the destructor-body consists of a block which specifies the statements to execute in order to destruct an instance of the class. A destructor-body corresponds exactly to the method-body of an instance method with a void return type (§10.5.8).

Destructors are not inherited. Thus, a class has no destructors other than the one which may be declared in that class.

Since a destructor is required to have no parameters, it cannot be overloaded, so a class can have, at most, one destructor.

Destructors are invoked automatically, and cannot be invoked explicitly. An instance becomes eligible for destruction when it is no longer possible for any code to use that instance. Execution of the destructor for the instance may occur at any time after the instance becomes eligible for destruction. When an instance is destructed, the destructors in that instance’s inheritance chain are called, in order, from most derived to least derived. A destructor may be executed on any thread. For further discussion of the rules that govern when and how a destructor is executed, see §3.9.

The output of the example

using System;

class A
{
   ~A() {
      Console.WriteLine("A's destructor");
   }
}

class B: A
{
   ~B() {
      Console.WriteLine("B's destructor");
   }
}

class Test
{
   static void Main() {
      B b = new B();
      b = null;
      GC.Collect();
      GC.WaitForPendingFinalizers();
   }
}

is

B’s destructor
A’s destructor

since destructors in an inheritance chain are called in order, from most derived to least derived.

Destructors are implemented by overriding the virtual method Finalize on System.Object. C# programs are not permitted to override this method or call it (or overrides of it) directly. For instance, the program

class A
{
   override protected void Finalize() {}  // error

   public void F() {
      this.Finalize();                    // error
   }
}

contains two errors.

The compiler behaves as if this method, and overrides of it, do not exist at all. Thus, this program:

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

is valid, and the method shown hides System.Object’s Finalize method.

For a discussion of the behavior when an exception is thrown from a destructor, see §16.3.